import math
import operator
import sets
import sys

import pygame
import pygame.font
import pygame.display
import pygame.time

sys.path.append("..\\af2d")
import af2d
from af2d import Vector2
from af2d import Rect2

import simulation

_DISPLAY_WIDTH = 1024
_DISPLAY_HEIGHT = 768

_FULL_SCREEN = 0
_WINDOWED = 1
_DISPLAY_MODE = _WINDOWED

_TARGET_FRAME_RATE = 32
_TIME_DELTA = 1.0 / float(_TARGET_FRAME_RATE)

_SCROLL_REGION_THICKNESS = _DISPLAY_WIDTH / 40

_MINI_MAP_DISPLAY_WIDTH = 150
_MINI_MAP_DISPLAY_HEIGHT = 150
_MINI_MAP_WORLD_WIDTH = 2000
_MINI_MAP_WORLD_HEIGHT = 2000

_MINI_MAP_DISPLAY_RECT = Rect2(
   _DISPLAY_WIDTH - (_SCROLL_REGION_THICKNESS + _MINI_MAP_DISPLAY_WIDTH),
   _DISPLAY_HEIGHT - _SCROLL_REGION_THICKNESS, _MINI_MAP_DISPLAY_WIDTH, -_MINI_MAP_DISPLAY_HEIGHT)

_MINI_MAP_WORLD_RECT = Rect2(
  -(_MINI_MAP_WORLD_WIDTH / 2), -(_MINI_MAP_WORLD_HEIGHT / 2), _MINI_MAP_WORLD_WIDTH, 
  _MINI_MAP_WORLD_HEIGHT)
  
_RENDER_MODE_STRATEGIC = 0
_RENDER_MODE_TACTICAL = 1

_STRATEGIC_UNIT_SIZE = 4

_CLICK_TIME_THRESHOLD_MS = 300
_CLICK_DISTANCE_THRESHOLD = 5


###########################################


_GLOBAL_SELECTION_SET = sets.Set()


###########################################


# !!! Move this to another module
class EntityRenderPeer(object):
    def __init__(self, entity):
        self._entity = entity
    
    def Render(self, surface, view_state, selected=False):
        assert self._entity
        if _RENDER_MODE_TACTICAL == view_state.GetRenderMode():
            self._RenderTactical(self._entity, surface, view_state, selected)
        else:
            self._RenderStrategic(self._entity, surface, view_state, selected)
            
    def _RenderTactical(self, entity, surface, view_state, selected):
        pass
    
    def _RenderStrategic(self, entity, surface, view_state, selected):
        pass


class PygameUnitRenderPeer(EntityRenderPeer):
    def __init__(self, unit):
        assert isinstance(unit, simulation.Unit)
        super(PygameUnitRenderPeer, self).__init__(unit)
    
    def _RenderTactical(self, unit, surface, view_state, selected):
        pygame.draw.circle(surface, (255, 255, 255), 
                           view_state.WorldToDisplay(unit.position).int_tuple, 5)
        if selected:
            pygame.draw.circle(
                surface, (0, 255, 0), 
                view_state.WorldToDisplay(unit.position).int_tuple, 7, 1)

    def _RenderStrategic(self, unit, surface, view_state, selected):
        color = (150, 150, 150)
        if selected:
            color = (255, 255, 0)
        
        pos = view_state.WorldToDisplay(unit.position)
        pygame.draw.rect(
          surface, color, 
          pygame.Rect(
            pos.x - (_STRATEGIC_UNIT_SIZE / 2), 
            pos.y - (_STRATEGIC_UNIT_SIZE / 2),
            _STRATEGIC_UNIT_SIZE, _STRATEGIC_UNIT_SIZE),
          1)


class PygameGameState(simulation.GameState):
    def __init__(self):
        super(PygameGameState, self).__init__()
        self.SetAddEntityCallback(self.AddEntityCallback)
        
    def AddEntityCallback(self, entity):
        if isinstance(entity, simulation.Unit):
            unit_render_peer = PygameUnitRenderPeer(entity)
            entity.user_data = unit_render_peer
        else:
            raise '!!! unknown entity type'
 
        
class InputAdapter:
    _LEFT_MOUSE_BUTTON = 0
    _MIDDLE_MOUSE_BUTTON = 1
    _RIGHT_MOUSE_BUTTON = 2
    _MOUSE_WHEEL_FORWARD = 3
    _MOUSE_WHEEL_BACK = 4
    
    _DOWN = True
    _UP = False
    
    class InputState(object):
        def __init__(self):
            self._mouse_button_states = {}
            self._key_states = {}
            self._mouse_position = None
            
        def GetMouseButtonState(self, button):
            return self._mouse_button_states.get(button, 
                                                 (InputAdapter._UP, None))
        
        def GetKeyState(self, key):
            return self._key_states.get(key, InputAdapter._UP)
        
        def GetMousePosition(self):
            return self._mouse_position
        
        def MouseButtonDown(self, button, pos):
            self._mouse_button_states[button] = (InputAdapter._DOWN, pos)

        def MouseButtonUp(self, button, pos):
            self._mouse_button_states[button] = (InputAdapter._UP, pos)
            
        def MouseMoved(self, pos):
            self._mouse_position = pos
            
        def KeyDown(self, key):
            self._key_states[key] = InputAdapter._DOWN
        
        def KeyUp(self, key):
            self._key_states[key] = InputAdapter._UP
            
    class Operation(object):
        def __init__(self, done_callback):
            self._done_callback = done_callback
            
        def _InvokeDoneCallback(self):
            self._done_callback()
            
        def TestBegin(self, input_state):
            pass
        
        def MouseButtonDown(self, button, display_pos, time_ms):
            pass
        
        def MouseButtonUp(self, button, display_pos, time_ms):
            pass
        
        def MouseMoved(self, display_pos, time_ms):
            pass
        
        def KeyDown(self, key, time_ms):
            pass
        
        def KeyUp(self, key, time_ms):
            pass
        
        def RenderOverlay(self, surface):
            pass
        
    class SelectOperation(Operation):
        def __init__(self, done_callback, game_state, view_state):
            super(InputAdapter.SelectOperation, self).__init__(done_callback)
            self._game_state = game_state
            self._view_state = view_state
            self._first_display_pos = None
            self._second_display_pos = None
            self._begin_time_ms = None
            
        def TestBegin(self, input_state):
            left_button_state, pos = input_state.GetMouseButtonState(
                InputAdapter._LEFT_MOUSE_BUTTON)
            return InputAdapter._DOWN == left_button_state
        
        def MouseButtonDown(self, button, display_pos, time_ms):
            if (InputAdapter._LEFT_MOUSE_BUTTON == button):
                self._first_display_pos = display_pos
                self._second_display_pos = display_pos
                self._begin_time_ms = time_ms
        
        def MouseButtonUp(self, button, display_pos, time_ms):
            if (InputAdapter._LEFT_MOUSE_BUTTON == button):
                self._second_display_pos = display_pos
                time_diff = time_ms - self._begin_time_ms
                distance = af2d.Distance(self._first_display_pos, 
                                         self._second_display_pos)
                if ((time_diff < _CLICK_TIME_THRESHOLD_MS) and 
                    (distance < _CLICK_DISTANCE_THRESHOLD)):
                    print 'click selection not implemented'
                else:
                    display_rect = af2d.VectorsToRect(self._first_display_pos,
                                                      self._second_display_pos)
                    world_rect = self._view_state.DisplayToWorldRect(
                                     display_rect)
                    global _GLOBAL_SELECTION_SET
                    _GLOBAL_SELECTION_SET = (
                        self._game_state.GetEntitiesInRect(world_rect))
            self._InvokeDoneCallback()
                
        def MouseMoved(self, display_pos, time_ms):
            self._second_display_pos = display_pos
        
        def RenderOverlay(self, surface):
            display_rect = AfVectorsToPygameRect(self._first_display_pos,
                                             self._second_display_pos)
            pygame.draw.rect(surface, (255, 255, 0), display_rect, 1)
            
    class MoveOperation(Operation):
        def __init__(self, done_callback, game_state, view_state):
            super(InputAdapter.MoveOperation, self).__init__(done_callback)
            self._game_state = game_state
            self._view_state = view_state
        
        def TestBegin(self, input_state):
            right_button_state, pos = input_state.GetMouseButtonState(
                InputAdapter._RIGHT_MOUSE_BUTTON)
            return InputAdapter._DOWN == right_button_state
        
        def MouseButtonDown(self, button, display_pos, time_ms):
            if (InputAdapter._RIGHT_MOUSE_BUTTON == button):
                destination = self._view_state.DisplayToWorld(display_pos)
                self._game_state.IssueUnitMoveOrder(_GLOBAL_SELECTION_SET,
                                                    destination)
                                                    
        def MouseButtonUp(self, button, display_pos, time_ms):
            if (InputAdapter._RIGHT_MOUSE_BUTTON == button):
                self._InvokeDoneCallback()
                
    class DrawPathOperation(Operation):
        def __init__(self, done_callback, game_state, view_state):
            super(InputAdapter.DrawPathOperation, self).__init__(done_callback)
            self._game_state = game_state
            self._view_state = view_state
            
        def TestBegin(self, input_state):
            left_button_state, pos = input_state.GetMouseButtonState(
                InputAdapter._LEFT_MOUSE_BUTTON)
            shift_key_down = (InputAdapter._DOWN == 
                              input_state.GetKeyState(pygame.K_RSHIFT)
                              or (InputAdapter._DOWN == 
                                  input_state.GetKeyState(pygame.K_LSHIFT)))
            if InputAdapter._DOWN == left_button_state and shift_key_down:
                self._path = simulation.Path()
                self._next_display_point = None
                return True
            else:
                return False
                    
        def MouseButtonDown(self, button, display_pos, time_ms):
            if (InputAdapter._LEFT_MOUSE_BUTTON == button):
                point = self._view_state.DisplayToWorld(display_pos)
                self._path.points.append(point)
                self._next_display_point = display_pos
            elif (InputAdapter._RIGHT_MOUSE_BUTTON == button):
                self._game_state.AddPath(self._path)
                self._InvokeDoneCallback()
                
        def MouseMoved(self, display_pos, time_ms):
            self._next_display_point = display_pos
          
        def RenderOverlay(self, surface):
            self._view_state.RenderPath(surface, self._path, (0, 255, 0),
                                        self._next_display_point, (255, 255, 0))
            
            if len(self._path.points):
                last_display_point = self._view_state.WorldToDisplay(
                    self._path.points[0])
                for point in self._path.points[1:]:
                    display_point = self._view_state.WorldToDisplay(point)
                    pygame.draw.line(surface, (0, 255, 0), 
                                     last_display_point.tuple, 
                                     display_point.tuple)
                    last_display_point = display_point
                pygame.draw.line(surface, (255, 255, 0), last_display_point.tuple,
                                 self._next_display_point.tuple)
                        
    def __init__(self, game_state, view_state, enable_mouse_scroll):
        self._game_state = game_state
        self._view_state = view_state
        self._scroll_regions = []
        
        if (enable_mouse_scroll):
            horiz_sr_width = _DISPLAY_WIDTH - (2 * _SCROLL_REGION_THICKNESS)
            vert_sr_height = _DISPLAY_HEIGHT - (2 * _SCROLL_REGION_THICKNESS)
            
            self._scroll_regions = [
                # Left edge
                (Rect2(0, _SCROLL_REGION_THICKNESS, _SCROLL_REGION_THICKNESS, vert_sr_height),
                 Vector2(-1, 0)),
                # Right edge
                (Rect2(_DISPLAY_WIDTH - _SCROLL_REGION_THICKNESS, _SCROLL_REGION_THICKNESS, 
                       _SCROLL_REGION_THICKNESS, vert_sr_height), 
                 Vector2(1, 0)), 
                # Top edge 
                (Rect2(_SCROLL_REGION_THICKNESS, 0, horiz_sr_width, _SCROLL_REGION_THICKNESS), 
                 Vector2(0, 1)),
                # Bottom edge 
                (Rect2(_SCROLL_REGION_THICKNESS, _DISPLAY_HEIGHT - _SCROLL_REGION_THICKNESS, 
                       horiz_sr_width, _SCROLL_REGION_THICKNESS), 
                 Vector2(0, -1)),
                # Top left corner
                (Rect2(0, 0, _SCROLL_REGION_THICKNESS, _SCROLL_REGION_THICKNESS), Vector2(-1, 1).Unit()),
                # Bottom left corner
                (Rect2(0, _DISPLAY_HEIGHT - _SCROLL_REGION_THICKNESS, _SCROLL_REGION_THICKNESS, 
                       _SCROLL_REGION_THICKNESS), 
                 Vector2(-1, -1).Unit()),
                 
                # Top right corner
                (Rect2(_DISPLAY_WIDTH - _SCROLL_REGION_THICKNESS, 0, _SCROLL_REGION_THICKNESS, 
                 _SCROLL_REGION_THICKNESS), 
                 Vector2(1, 1).Unit()),
                # Bottom right corner
                (Rect2(_DISPLAY_WIDTH - _SCROLL_REGION_THICKNESS, 
                       _DISPLAY_HEIGHT - _SCROLL_REGION_THICKNESS, _SCROLL_REGION_THICKNESS, 
                       _SCROLL_REGION_THICKNESS), 
                 Vector2(1, -1).Unit()),
            ]
        
        self.__input_state = InputAdapter.InputState()
        self.__operations = [
            InputAdapter.DrawPathOperation(
                self.__EndOperationCallback, self._game_state, self._view_state),
            InputAdapter.SelectOperation(
                self.__EndOperationCallback, self._game_state, self._view_state),
            InputAdapter.MoveOperation(
                self.__EndOperationCallback, self._game_state, self._view_state),
        ]
        self.__current_operation = None
        
    #######################################################
    # Public interface (can also be called from subclasses)
    
    # This method should be invoked every frame
    def DoEachFrame(self):
        for (rect, direction) in self._scroll_regions:
            if rect.Contains(self._GetCursorPos()):
                self._view_state.SmoothScroll(direction)
                break
            
    # This method should be invoked after everything else has been rendered
    def RenderOverlay(self, surface):
        if self.__current_operation:
            self.__current_operation.RenderOverlay(surface)
            
    def GetDisplayRect(self):
        return self._view_state.GetDisplayRect()
    
    
    #################################
    # Subclass should implement this.
    
    def _GetCursorPos(self):
        raise 'Not implemented'
        
    
    #############################
    # Subclass should call these.
    
    def _MouseButtonDown(self, button, pos, time_ms):
        self.__input_state.MouseButtonDown(button, pos)
        self.__UpdateOperation()
        if self.__current_operation:
            self.__current_operation.MouseButtonDown(button, pos, time_ms)
        
    def _MouseButtonUp(self, button, pos, time_ms):
        self.__input_state.MouseButtonUp(button, pos)
        self.__UpdateOperation()
        if self.__current_operation:
            self.__current_operation.MouseButtonUp(button, pos, time_ms)
            
    def _MouseMoved(self, pos, time_ms):
        self.__input_state.MouseMoved(pos)
        self.__UpdateOperation()
        if self.__current_operation:
            self.__current_operation.MouseMoved(pos, time_ms)
        
    def _MouseWheelRolled(self, button, pos, time_ms):
        if (button == InputAdapter._MOUSE_WHEEL_FORWARD):
            self._view_state.ZoomIn()
        else:
            self._view_state.ZoomOut()
    
    def _KeyDown(self, key, time_ms):
        self.__input_state.KeyDown(key)
        if key == pygame.K_UP:
            self._view_state.ClickNorth()
        elif key == pygame.K_DOWN:
            self._view_state.ClickSouth()
        elif key == pygame.K_RIGHT:
            self._view_state.ClickEast()
        elif key == pygame.K_LEFT:
            self._view_state.ClickWest()
        else:
            self.__UpdateOperation()
            if self.__current_operation:
                self.__current_operation.KeyDown(key, time_ms)
                
    def _KeyUp(self, key, time_ms):
        self.__input_state.KeyUp(key)
        self.__UpdateOperation()
        if self.__current_operation:
            self.__current_operation.KeyUp(key, time_ms)
            
    #################################
    # Private event handling methods.
    
    def __UpdateOperation(self):
        if not self.__current_operation:
            for operation in self.__operations:
                if operation.TestBegin(self.__input_state):
                    self.__current_operation = operation
                    break
    
    # Called by operations when they complete
    def __EndOperationCallback(self):
        self.__current_operation = None


class PygameInputAdapter(InputAdapter):    
    def __init__(self, game_state, view_state, enable_mouse_scroll):
        InputAdapter.__init__(self, game_state, view_state, enable_mouse_scroll)
        
    def _GetCursorPos(self):
        pos = pygame.mouse.get_pos()
        return Vector2(pos[0], pos[1])
        
    def ProcessEvents(self, events, time_ms):
        def _IsButtonEvent(event):
            return event.button in [1, 2, 3]
    
        def _FromPygameMouseButton(button):
            if button == 1: return InputAdapter._LEFT_MOUSE_BUTTON
            elif button == 2: return InputAdapter._MIDDLE_MOUSE_BUTTON
            elif button == 3: return InputAdapter._RIGHT_MOUSE_BUTTON
            elif button == 4: return InputAdapter._MOUSE_WHEEL_FORWARD
            elif button == 5: return InputAdapter._MOUSE_WHEEL_BACK
            else: raise 'Undefined mouse button: ' + str(button)
    
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN:
                if (_IsButtonEvent(event)):
                    self._MouseButtonDown(_FromPygameMouseButton(event.button), 
                                          Vector2(event.pos[0], event.pos[1]),
                                          time_ms)
                else:
                    self._MouseWheelRolled(_FromPygameMouseButton(event.button),
                                           Vector2(event.pos[0], event.pos[1]),
                                           time_ms)
            elif event.type == pygame.MOUSEBUTTONUP:
                if (_IsButtonEvent(event)):
                    self._MouseButtonUp(_FromPygameMouseButton(event.button), 
                                        Vector2(event.pos[0], event.pos[1]),
                                        time_ms)
            elif event.type == pygame.MOUSEMOTION:
                self._MouseMoved(Vector2(event.pos[0], event.pos[1]), time_ms)
            elif event.type == pygame.KEYDOWN:
                self._KeyDown(event.key, time_ms)
            elif event.type == pygame.KEYUP:
                self._KeyUp(event.key, time_ms)
        
        self.DoEachFrame()
        
        
def AfToPygameRect(af_rect):
    left = min(af_rect.left, af_rect.right)
    top = min(af_rect.top, af_rect.bottom)
    width = abs(af_rect.width)
    height = abs(af_rect.height)
    return pygame.Rect(left, top, width, height)

def AfVectorsToPygameRect(af_vec1, af_vec2):
    return pygame.Rect(min(af_vec1.x, af_vec2.x),
                       min(af_vec1.y, af_vec2.y),
                       abs(af_vec1.x - af_vec2.x),
                       abs(af_vec1.y - af_vec2.y))


class ViewState(object):
    def __init__(self, world_rect, display_rect):
        self._world_rect = world_rect
        self._display_rect = display_rect
        
    def GetWorldRect(self):
        return self._world_rect
    
    def GetDisplayRect(self):
        return self._display_rect
        
    def GetRenderMode(self):
        return _RENDER_MODE_TACTICAL
    
    def ClickNorth(self):
        self._world_rect.Translate(Vector2(0, self._world_rect.height / 4.0))
        
    def ClickSouth(self):
        self._world_rect.Translate(Vector2(0, -self._world_rect.height / 4.0))
        
    def ClickEast(self):
        self._world_rect.Translate(Vector2(self._world_rect.width / 4.0, 0))
        
    def ClickWest(self):
        self._world_rect.Translate(Vector2(-self._world_rect.width / 4.0, 0))
        
    def SmoothScroll(self, direction):
        self._world_rect.Translate(direction * (self._world_rect.width) * _TIME_DELTA)
        
    def ZoomIn(self):
        self._world_rect.ScaleAboutCenter(0.5)
        
    def ZoomOut(self):
        self._world_rect.ScaleAboutCenter(2.0)
        
    def WorldToDisplay(self, point):
        return af2d.RectTransform(point, self._world_rect, self._display_rect)
        
    def DisplayToWorld(self, point):
        return af2d.RectTransform(point, self._display_rect, self._world_rect)
    
    def WorldToDisplayXDistance(self, d):
        return (float(d) / self._world_rect.width) * self._display_rect.width
    
    def DisplayToWorldXDistance(self, d):
        return (float(d) / self._display_rect.width) * self._world_rect.width
    
    def WorldToDisplayYDistance(self, d):
        return (float(d) / self._world_rect.height) * self._display_rect.height
    
    def WorldToDisplayRect(self, rect):
        return af2d.RectTransformRect(rect, self._world_rect, self._display_rect)
    
    def DisplayToWorldRect(self, rect):
        return af2d.RectTransformRect(rect, self._display_rect, self._world_rect)
    
    def _RenderGridHelper(self, surface, color, min_world_spacing, 
                          min_display_spacing, scale_factor):
        power = round(math.log(min_display_spacing / 
            self.WorldToDisplayXDistance(min_world_spacing), scale_factor) + 0.5)
        world_grid_spacing = min_world_spacing * (scale_factor ** power)

        world_x = round((self._world_rect.left / world_grid_spacing) + 0.5) * world_grid_spacing
        while(world_x < self._world_rect.right):
            v = self.WorldToDisplay(Vector2(world_x, 0))
            pygame.draw.line(surface, color, (v.x, self._display_rect.bottom), 
                             (v.x, self._display_rect.top))
            world_x += world_grid_spacing
        
        world_y = round((self._world_rect.bottom / world_grid_spacing) + 0.5) * world_grid_spacing
        while(world_y < self._world_rect.top):
            v = self.WorldToDisplay(Vector2(0, world_y))
            pygame.draw.line(surface, color, (self._display_rect.left, v.y), 
                             (self._display_rect.right, v.y))
            world_y += world_grid_spacing
        
    
    def _RenderGrid(self, surface):
        MAJOR_MIN_DISPLAY_GRID_SPACING = 100
        MAJOR_MIN_WORLD_SPACING = 10
        MINOR_MIN_DISPLAY_GRID_SPACING = 10
        MINOR_MIN_WORLD_SPACING = 1
        GRID_SCALE_FACTOR = 10
        self._RenderGridHelper(surface, (50, 50, 50), MINOR_MIN_WORLD_SPACING, 
                         MINOR_MIN_DISPLAY_GRID_SPACING, GRID_SCALE_FACTOR)
        self._RenderGridHelper(surface, (100, 100, 200), MAJOR_MIN_WORLD_SPACING,
                         MAJOR_MIN_DISPLAY_GRID_SPACING, GRID_SCALE_FACTOR)
                         
        origin = self.WorldToDisplay(Vector2(0, 0))
        x_unit = self.WorldToDisplay(Vector2(1, 0))
        y_unit = self.WorldToDisplay(Vector2(0, 1))
        pygame.draw.line(surface, (0, 255, 0), origin.int_tuple, x_unit.int_tuple)
        pygame.draw.line(surface, (0, 255, 0), origin.int_tuple, y_unit.int_tuple)
        
    def _RenderBorder(self, surface, color):
        pygame.draw.rect(surface, color, AfToPygameRect(self.GetDisplayRect()), 1)
        
    def RenderBackground(self, surface):
        pass
    
    def RenderForeground(self, surface):
        pass
    
    def RenderPath(self, surface, world_path, world_path_color, 
                   feedbackDisplayPoint=None, feedback_color=None):
        if world_path.points:
            last_display_point = self.WorldToDisplay(world_path.points[0])
            for point in world_path.points[1:]:
                display_point = self.WorldToDisplay(point)
                pygame.draw.line(surface, world_path_color, 
                                 last_display_point.tuple, 
                                 display_point.tuple)
                last_display_point = display_point
            if feedbackDisplayPoint:    
                pygame.draw.line(surface, feedback_color, 
                                 last_display_point.tuple, 
                                 feedbackDisplayPoint.tuple)
    
    def Render(self, surface, game_state, selection_set):
        self.RenderBackground(surface)
        for entity in game_state.GetEntitiesInRect(self.GetWorldRect()):
            render_peer = entity.user_data
            render_peer.Render(surface, self, selected=entity in selection_set)
        for path in game_state.GetPathsInRect(self.GetWorldRect()):
            self.RenderPath(surface, path, (50, 155, 50))
        self.RenderForeground(surface)

class TacticalViewState(ViewState):
    def __init__(self, world_rect, display_rect):
        ViewState.__init__(self, world_rect, display_rect)
        
    def RenderBackground(self, surface):
        self._RenderGrid(surface)
    

class StrategicViewState(ViewState):
    def __init__(self, view_states_to_render, world_rect, display_rect):
        ViewState.__init__(self, world_rect, display_rect)
        self._view_states_to_render = view_states_to_render
        
    def GetRenderMode(self):
        return _RENDER_MODE_STRATEGIC
    
    def RenderForeground(self, surface):
        for view_state in self._view_states_to_render:
            world_rect = self.WorldToDisplayRect(view_state.GetWorldRect())
            pygame.draw.rect(surface, (255, 255, 0), AfToPygameRect(world_rect), 1)
        self._RenderBorder(surface, (255, 255, 255))
    
####################

def ProcessInput(input_adapter):
    continue_game = True
    events = pygame.event.get()
    game_events = []
    for event in events:
        if (event.type == pygame.QUIT or 
            (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE)):
            continue_game = False
        else:
            game_events.append(event)
    input_adapter.ProcessEvents(game_events, pygame.time.get_ticks())
    return continue_game

    
def Render(surface, font, input_adapters, game_state, view_states, clock=None):
    for view_state in view_states:
        display_rect = AfToPygameRect(view_state.GetDisplayRect())
        surface.set_clip(display_rect)
        surface.fill((0, 0, 0), display_rect)    
        view_state.Render(surface, game_state, _GLOBAL_SELECTION_SET)
        
    for input_adapter in input_adapters:
        display_rect = AfToPygameRect(input_adapter.GetDisplayRect())
        surface.set_clip(display_rect)
        input_adapter.RenderOverlay(surface)
    
    surface.set_clip(surface.get_rect())

    # render frame rate
    if clock:
        temp_surface = font.render(str(clock.get_fps()), 1, (255, 0, 0))
        surface.blit(temp_surface, (20, _DISPLAY_HEIGHT - 50))

    # render number of entities
    temp_surface = font.render(str(game_state.GetNumEntities()), 1, (255, 0, 0))
    surface.blit(temp_surface, (_DISPLAY_WIDTH - 50, _DISPLAY_HEIGHT - 50))

    pygame.display.flip()


def ComputeTargetCycle(target_cycle, leftover_cycles, clock):
    ms_passed = clock.tick()
    cycles_passed = ((float(ms_passed) / 1000.0) * float(_TARGET_FRAME_RATE)) + leftover_cycles
    target_cycle += int(cycles_passed)
    leftover_cycles = cycles_passed - int(cycles_passed)
    return target_cycle, leftover_cycles


def GameLoop(surface, font, input_adapters, game_state, view_states):
    actual_cycle = 0
    target_cycle = 0
    clock = pygame.time.Clock()
    leftover_cycles = 0
    continue_game = True

    # clock.tick()
    while continue_game:
        Render(surface, font, input_adapters, game_state, view_states, clock)
        target_cycle, leftover_cycles = ComputeTargetCycle(target_cycle, leftover_cycles, clock)
        while (target_cycle > actual_cycle):
            for input_adapter in input_adapters:
                continue_game &= ProcessInput(input_adapter)
            game_state.SimulationStep(_TIME_DELTA)
            actual_cycle += 1


####################


def main():
    pygame.init()
    pygame.display.init()
    display_flags = 0  # pygame.HWSURFACE | pygame.DOUBLEBUF <- screws up cursor in full screen mode
    enable_mouse_scroll = False
    if _FULL_SCREEN == _DISPLAY_MODE:
        display_flags |= pygame.FULLSCREEN
        enable_mouse_scroll = True
    surface = pygame.display.set_mode((_DISPLAY_WIDTH, _DISPLAY_HEIGHT), display_flags)
    pygame.font.init()
    font = pygame.font.Font("C:\\Windows\\Fonts\\arial.ttf", 30)
    
    game_state = PygameGameState()
    
    # Initialize game state.
    for world_x in range(-45, 55, 10):
        for world_y in range(-45, 55, 10):
            game_state.AddUnit(Vector2(world_x, world_y))
    
    # Initialize the GUI
    main_view_state = TacticalViewState(
        Rect2(-200, -150, 400, 300),
        Rect2(0, _DISPLAY_HEIGHT, _DISPLAY_WIDTH, - _DISPLAY_HEIGHT))
    mini_map_view_state = StrategicViewState(
        [main_view_state], _MINI_MAP_WORLD_RECT, _MINI_MAP_DISPLAY_RECT)
    input_adapter = PygameInputAdapter(game_state, main_view_state, 
                                       enable_mouse_scroll)
    
    # Start the main game loop
    GameLoop(surface, font, [input_adapter], game_state, 
             [main_view_state, mini_map_view_state])


if __name__ == '__main__':
    main()